home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
PC World Komputer 2010 April
/
PCWorld0410.iso
/
pluginy Firefox
/
2410
/
2410.xpi
/
chrome
/
content
/
foxmarks-core.js
< prev
next >
Wrap
Text File
|
2010-01-28
|
30KB
|
881 lines
/*
Copyright 2007 Foxmarks Inc.
foxmarks-core.js: implements core sync & merge algorithms.
*/
function EnumerateDeletedNodes(cs, baseline, current) {
// Given a delete command, determine which of its desecendant nodes
// were actually deleted, and attach that list of nids.
// Some children may have been moved prior to the delete, so they
// would survive in the curernt nodeset. Those survivors are not
// included in the attached list.
var deleted = null;
function EnumerateInternal(nid) {
if (!current.Node(nid, false, true)) {
deleted.push(nid);
}
var node = baseline.Node(nid);
if (node.children) {
node.children.forEach(function(nid) { EnumerateInternal(nid) } );
}
}
cs.set.forEach(function(command) {
if (command.action == 'delete') {
command.deleted = [];
deleted = command.deleted;
EnumerateInternal(command.nid);
}
} );
}
function CleanDeletedNodes(cs) {
cs.set.forEach(function(cmd) {
if (cmd.action == 'delete') {
delete cmd.deleted;
}
delete cmd.fixed;
} );
}
function CombinePrePost(cs) {
cs.set = cs.pre.concat(cs.set).concat(cs.post);
delete cs.pre;
delete cs.post;
}
function OpenConflictDialog(url, input) {
// find a browser window to use as a parent for openDialog
var wm = Cc['@mozilla.org/appshell/window-mediator;1'].getService();
var wmi = wm.QueryInterface(Ci.nsIWindowMediator);
var top = wmi.getMostRecentWindow("foxmarks:progress");
// If we can't find one of our own windows, try any window.
if (!top) {
top = wmi.getMostRecentWindow(null);
if (!top) {
// We couldn't find a window, so pretend user canceled.
return null;
}
}
var output = {};
top.openDialog(url, "_blank", "chrome,dialog,modal,centerscreen",
output, input);
return output.selection;
}
var DETECTORS = {
// Format for naming of these detectors is localaction_serveraction.
insert_insert: function(local, server, localNS, serverNS) {
if (local.nid == server.nid) { // items created with same nid. Scary!
var conflict = [];
forEach(local.args, function(value, attr) {
if (server.args[attr] && !DETECTORS.HARMLESS_ATTRS[attr] &&
value != server.args[attr]) {
conflict.push(attr + ":" + value);
}
} );
forEach(server.args, function(value, attr) {
if (local.args[attr] && !DETECTORS.HARMLESS_ATTRS[attr] &&
value != local.args[attr]) {
conflict.push(attr + ":" + value);
}
} );
if (conflict.length) {
var lnode = localNS.Node(local.nid);
var snode = serverNS.Node(server.nid);
var rv = localNS.handleNidConflict(lnode, snode, conflict);
showedUI = true;
if (rv == "local") {
return ["overwrite", "local"];
} else if (rv == "server") {
return ["overwrite", "server"];
} else if (rv == "same") {
return ["drop", "both"];
} else {
throw 2;
}
} else {
return ["drop", "both"];
}
}
if (local.args.pnid == server.args.pnid &&
local.args.bnid == server.args.bnid) {
return ["transit", "server"];
}
return null;
},
insert_move: function(local, server) {
if (local.args.pnid != server.args.pnid &&
local.args.bnid == server.nid) {
return ["nextsib", "left"];
}
if (local.nid != server.nid &&
local.args.pnid == server.args.pnid &&
local.args.bnid == server.args.bnid) {
return ["transit", "server"];
}
return null;
},
insert_reorder: function(local, server, lns, sns) {
if (local.args.pnid == sns.Node(server.nid).pnid &&
local.args.bnid == server.args.bnid && !server.fixed) {
return ["transit", "server"];
}
if (local.args.pnid == sns.Node(server.nid).pnid &&
local.args.bnid == server.nid) {
return ["nextsib", "left"];
}
return null;
},
insert_update: function(local, server) {
return null;
},
delete_insert: function(local, server) {
if (local.deleted.indexOf(server.args.bnid) >= 0) {
return ["nextsib", "right"];
}
if (local.deleted.indexOf(server.args.pnid) >= 0) {
return ["delinsert", "left"];
}
return null;
},
delete_delete: function(local, server) {
if (local.nid == server.nid) {
return ["drop", "both"];
}
if (local.deleted.indexOf(server.nid) >= 0) {
return ["drop", "right"];
}
if (server.deleted.indexOf(local.nid) >= 0) {
return ["drop", "left"];
}
return null;
},
delete_move: function(local, server) {
if (local.deleted.indexOf(server.args.bnid) >= 0) {
return ["nextsib", "right"];
}
if (local.deleted.indexOf(server.nid) >= 0) {
return ["delmoveout", "left"];
}
if (local.deleted.indexOf(server.args.pnid) >= 0) {
return ["delmovein", "left"];
}
return null;
},
delete_reorder: function(local, server) {
if (local.deleted.indexOf(server.nid) >= 0) {
return ["drop", "right"];
}
if (local.nid == server.args.bnid) {
return ["nextsib", "right"];
}
return null;
},
delete_update: function(local, server) {
if (local.deleted.indexOf(server.nid) >= 0) {
// A modified node was deleted. If the modifications
// were significant, we've got a real conflict. Otherwise,
// drop the modification and proceed with the delete.
for (var a in server.args) {
if (server.args.hasOwnProperty(a) &&
!DETECTORS.HARMLESS_ATTRS[a]) {
return ["delupdate", "left"];
}
}
// The update was insignificant; drop it.
return ["drop", "right"];
}
return null;
},
move_move: function(local, server, localNS, serverNS) {
if (local.args.pnid == server.args.pnid) {
if (local.nid == server.nid) {
if (local.args.bnid == server.args.bnid) {
return ["drop", "both"];
} else {
return ["drop", "server"];
}
} else {
if (local.args.bnid == server.args.bnid) {
return ["transit", "server"];
}
}
} else {
if (local.nid == server.nid) {
var input = { item: localNS.Node(local.nid),
local: localNS.Node(local.args.pnid),
server: serverNS.Node(server.args.pnid) };
var rv = OpenConflictDialog(
"chrome://foxmarks/content/foxmarks-parentconflict.xul",
input);
showedUI = true;
if (rv == "local") {
return ["drop", "server"];
} else if (rv == "server") {
return ["drop", "local"];
} else {
throw 2;
}
} else {
if (local.nid == server.args.bnid) {
return ["nextsib", "right"];
} else if (local.args.bnid == server.nid) {
return ["nextsib", "left"];
}
}
}
return null;
},
move_reorder: function(local, server, lns, sns) {
if (local.nid == server.nid) {
return ["drop", "right"];
}
if (local.nid == server.args.bnid &&
local.args.pnid != sns.Node(server.nid).pnid &&
!server.fixed) {
return ["nextsib", "right"];
}
if (local.args.pnid == sns.Node(server.nid).pnid &&
local.args.bnid == server.args.bnid &&
!server.fixed) {
return ["transit", "server"];
}
return null;
},
move_update: function() {
return null;
},
reorder_reorder: function(local, server, lns, sns) {
if (lns.Node(local.nid).pnid != sns.Node(server.nid).pnid) {
return null;
}
if (local.nid == server.nid) {
if (local.args.bnid == server.args.bnid) {
if (!local.fixed) {
return ["drop", "both"];
} else {
return null;
}
} else {
// XXX: Must retarget in this case
return ["drop", "server"];
}
} else { // local.nid != server.nid
if (local.nid == server.args.bnid && !server.fixed) {
return ["commutereorder", "left"];
} else if (local.args.bnid == server.nid && !local.fixed) {
return ["commutereorder", "right"];
} else {
return null;
}
}
},
reorder_update: function() {
return null;
},
HARMLESS_ATTRS: { created: true, visited: true, modified: true, private: true,
icon: true },
update_update: function(local, server, localNS, serverNS) {
if (local.nid != server.nid)
return;
var conflicts = [];
forEach(local.args, function(value, attr) {
if (server.args[attr] && !DETECTORS.HARMLESS_ATTRS[attr] &&
value != server.args[attr]) {
conflicts.push(attr);
}
} );
// Special case: tag-only conflict
if (conflicts.length == 1 && conflicts[0] == 'tags') {
return ["drop", "server"];
}
// Special case: conflict on (hidden attributes of) ROOT
if (local.nid == NODE_ROOT) {
return ["drop", "server"];
}
if (conflicts.length) {
var lnode = localNS.Node(local.nid);
var snode = serverNS.Node(server.nid);
var rv = localNS.handleNidConflict(lnode, snode, conflicts);
showedUI = true;
if (rv == "local") {
return ["drop", "server"];
} else if (rv == "server") {
return ["drop", "local"];
} else if (rv == "same") {
return ["drop", "both"];
} else {
throw 2;
}
}
return null;
}
}
// Given a baseline nodeset, a local nodeset, and a server nodeset,
// generate two commandsets, one to be applied to the local nodeset,
// another to be applied to the server nodeset. Applying each commandset
// to the respective nodeset will result in the two nodsets becoming
// synchronized.
//
// Note that the routine operates asynchronously; the client-provided
// callback will be called with two parameters: localCS (the commandset
// to be applied to the server, and serverCS (the commandset to be applied
// to the local nodeset).
function Synchronize(baseline, localCS, serverCS, local, server, callback) {
EnumerateDeletedNodes(localCS, baseline, local);
EnumerateDeletedNodes(serverCS, baseline, server);
localCS.pre = [];
localCS.post = [];
serverCS.pre = [];
serverCS.post = [];
// Iterate over each local commmand, comparing it with each
// server command. Detect and resolve conflicts.
var iterations = 0;
var conflicts = {};
var showedUI = false;
for (var l = 0; l < localCS.set.length; ++l) {
var lc = localCS.set[l];
var result = null;
var breakOut = false;
for (var s = 0; s < serverCS.set.length; ++s) {
sc = serverCS.set[s];
var method = DETECTORS[lc.action + "_" + sc.action];
if (method) {
try {
result = method(lc, sc, local, server, false);
} catch (e) {
Xmarks.LogWrite("Synchronize: failed processing " +
lc.toSource() + ", " + sc.toSource());
Xmarks.LogWrite("Error is " + e.toSource());
throw e;
}
if (!result)
continue;
if (result[1] == 'left') result[1] = 'local';
else if (result[1] == 'right') result[1] = 'server';
} else {
method = DETECTORS[sc.action + "_" + lc.action];
if (!method) {
throw Error("Couldn't find conflict detector for " +
lc.action + " : " + sc.action);
}
try {
result = method(sc, lc, server, local, true);
} catch (e) {
Xmarks.LogWrite("Synchronize: failed processing (reversed) " +
sc.toSource() + ", " + lc.toSource());
Xmarks.LogWrite("Error is " + e.toSource());
throw e;
}
if (!result)
continue;
if (result[1] == 'left') result[1] = 'server';
else if (result[1] == 'right') result[1] = 'local';
}
conflicts[result[0]] = (conflicts[result[0]] || 0) + 1;
Xmarks.LogWrite(">>> Sync: Executing " + result[0] + "(" + result[1] +
") on " + lc.toSource() + " vs. " + sc.toSource());
switch (result[0]) {
case 'drop': do_drop(result[1]); break;
case 'overwrite': do_overwrite(result[1]); break;
case 'transit': do_transit(result[1]); break;
case 'nextsib': do_nextsib(result[1]); break;
case 'delinsert': do_delinsert(result[1]); break;
case 'delmovein': do_delmovein(result[1]); break;
case 'delmoveout': do_delmoveout(result[1]); break;
case 'delupdate': do_delupdate(result[1]); break;
case 'commutereorder': do_commutereorder(result[1]); break;
default: throw Error("Unknown sync operator " + result[0]);
}
if (breakOut) {
iterations += 1;
if (iterations >= 1000) {
throw 1013;
}
break;
}
}
}
// We have resolved all conflicts in commands
CleanDeletedNodes(localCS);
CleanDeletedNodes(serverCS);
CombinePrePost(localCS);
CombinePrePost(serverCS);
callback(localCS, serverCS, conflicts, showedUI);
return;
function do_overwrite(arg) {
switch (arg) {
case 'local':
serverCS.drop(s);
s = s - 1;
localCS.set[l].action = "update";
localCS.set[l].args.bnid = undefined;
localCS.set[l].args.pnid = undefined;
break;
case 'server':
localCS.drop(l);
l = l - 1;
serverCS.set[s].action = "update";
serverCS.set[s].args.bnid = undefined;
serverCS.set[s].args.pnid = undefined;
break;
default:
throw Error("Unknown sync parameter " + result[1]);
}
return;
}
function do_drop(arg) {
switch (arg) {
case 'both':
localCS.drop(l);
serverCS.drop(s);
l = l - 1;
breakOut = true;
// bust out of inner loop; decrement l and start there
break;
case 'local':
localCS.drop(l);
l = l - 1;
breakOut = true;
// bust out of inner loop; decrement l and start there
break;
case 'server':
serverCS.drop(s);
s = s - 1;
// decrement s and continue inner loop
break;
default:
throw Error("Unknown sync parameter " + result[1]);
}
return;
}
function do_transit(arg) {
switch (arg) {
case 'local':
lc.args.bnid = sc.nid;
l = l - 1;
breakOut = true;
// restart inner loop
break;
case 'server':
sc.args.bnid = lc.nid;
l = -1;
breakOut = true;
// restart outer loop
break;
default:
throw Error("Uknown sync parameter " + result[1]);
}
return;
}
function do_nextsib(arg) {
switch (result[1]) {
case 'local':
lc.args.bnid = local.NextSibling(lc.args.bnid, baseline);
l = l - 1;
breakOut = true;
// restart inner loop
break;
case 'server':
sc.args.bnid = server.NextSibling(sc.args.bnid, baseline);
l = -1;
breakOut = true;
// restart outer loop
break;
default:
throw Error("Unknown sync parameter " + result[1]);
}
return;
}
function do_delinsert(arg) {
// arg is local or server, determining which side
// originated the delete. The other side originated a
// conflicting insert. The desired end result is for the inserted
// node to be inserted into the nearest remaining ancestor of the
// original pnid. This is accomplished by modifying the existing
// insert command, changing its parent, and inserting a move command
// to the side that originated the delete which moves the already
// inserted node into the correct parent before performing the delete.
// Finally, we add reorders to the post-sequence so make sure that
// the inserted item ends up at the bottom of the folder.
var del = null;
var ins = null;
var delCS = null;
if (arg == "server") {
del = sc;
ins = lc;
delCS = serverCS;
} else {
del = lc;
ins = sc;
delCS = localCS;
}
// What's the ultimate destination for the insert?
var pnid = NearestAncestor(ins.args.pnid, baseline, local, server);
// Alter the insert command so that it winds up in a safe home.
ins.args.pnid = pnid;
ins.args.bnid = null;
// Move the inserted node aside before the delete
delCS.pre.push(new Command("move", ins.nid, { pnid: pnid } ));
// And now make sure it's add the end of the folder on both sides.
var cmd = new Command("reorder", ins.nid, { bnid: null } );
localCS.post.push(cmd);
serverCS.post.push(cmd);
// We've modified both commandsets; restart processing from the top.
l = -1;
breakOut = true;
return;
}
function do_delmovein(arg) {
// arg is local or server, determining which side
// originated the delete. The other side originated a
// conflicting move into the deleted subtree.
// The desired end result is for the moved
// node to be inserted into the nearest remaining ancestor of the
// original pnid. This is accomplished by modifying the existing
// move command, changing its parent, and inserting a move command
// to the side that originated the delete which moves the already
// inserted node into the correct parent before performing the delete.
var del = null;
var ins = null;
var delCS = null;
if (arg == "server") {
del = sc;
ins = lc;
delCS = serverCS;
} else {
del = lc;
ins = sc;
delCS = localCS;
}
// What's the ultimate destination for the move?
var pnid = NearestAncestor(ins.args.pnid, baseline, local, server);
// Alter the move command so that it winds up in a safe home.
ins.args.pnid = pnid;
ins.args.bnid = null;
// Move the inserted node aside before the delete
delCS.pre.push(new Command("move", ins.nid, { pnid: pnid } ));
// And now make sure it's add the end of the folder on both sides.
var cmd = new Command("reorder", ins.nid, { bnid: null } );
localCS.post.push(cmd);
serverCS.post.push(cmd);
// We've modified both commandsets; restart processing from the top.
l = -1;
breakOut = true;
return;
}
function do_delupdate(arg) {
// arg is local or server, determining which side
// originated the delete. The other side originated a
// conflicting update within the deleted subtree.
// The desired end result is for the updated node and its
// descendants to survive at the nearest remaining ancestor of the
// original destination. This is accomplished by resurrecting the
// modified node and its descendants and adding a move to the side
// from which the delete originated which moves the item to its new
// location.
var update = null;
var updateCS = null;
var moveNS = null;
var del = null;
var delNS = null
var delIndex = 0;
var updateIndex = 0;
if (arg == "server") {
update = lc;
updateCS = localCS;
updateNS = local;
del = sc;
delNS = server;
delCS = serverCS;
delIndex = s;
updateIndex = l;
} else {
update = sc;
updateCS = serverCS;
updateNS = server;
del = lc;
delNS = local;
delCS = localCS;
delIndex = l;
updateIndex = s;
}
// Resurrect the deleted nodes
var pnid = ResurrectNodes(update.nid, baseline, updateNS,
delNS, updateCS, updateIndex, del, delCS, delIndex, null);
// On the delete side, add a move of the referenced node
// ... but only if it's a different parent.
if (pnid != updateNS.Node(update.nid).pnid) {
delCS.pre.push( new Command("move", update.nid, { pnid: pnid } ));
}
// Push it to the end of its parent folder for safety
if(updateNS.OrderIsImportant()){
var cmd = new Command("reorder", update.nid, { bnid: null } );
localCS.post.push(cmd);
serverCS.post.push(cmd);
}
// We've modified both commandsets; restart processing from the top.
l = -1;
breakOut = true;
return;
}
function do_delmoveout(arg) {
// arg is local or server, determining which side
// originated the delete. The other side originated a
// conflicting move out of the deleted subtree.
// The desired end result is for the moved
// node to survive at the nearest remaining ancestor of the
// original destination. This is accomplished by replacing the existing
// move command with a (series of) insert(s) that resurrect the deleted
// nodes(s) in their correct position. We let delinsert deal with the
// case where the destination folder has also been deleted.
var move = null;
var moveCS = null;
var moveIndex = null;
var moveNS = null;
var del = null;
var delNS = null
var delCS = null;
var delIndex = 0;
if (arg == "server") {
move = lc;
moveCS = localCS;
moveIndex = l;
moveNS = local;
del = sc;
delNS = server;
delCS = serverCS;
delIndex = s;
} else {
move = sc;
moveCS = serverCS;
moveIndex = s;
moveNS = server;
del = lc;
delNS = local;
delCS = localCS;
delIndex = l;
}
// Remove the move command
moveCS.set.splice(moveIndex, 1);
// Resurrect the deleted nodes
ResurrectNodes(move.nid, baseline, moveNS, delNS, moveCS, moveIndex,
del, delCS, delIndex, move.args.bnid);
// We've modified both commandsets; restart processing from the top.
l = -1;
breakOut = true;
return;
}
function do_commutereorder(arg) {
// We've encountered R(x,y) vs. R(y,z).
// arg determines the side receiving the additional command R(x,y).
// The resolution is to add R(x,y) as a simulated command
// after R(y,z). We also mark the original R(x,y) as fixed
// so we don't trigger again and so we don't drop it believing
// that it's redundant. It would be nice to handle this in
// the post-sequence, but, unfortunately, we need to get the
// order straightened out before further moves and inserts happen.
var c = arg == "local" ? sc : lc;
var set = arg == "local" ? localCS.set : serverCS.set;
var i = arg == "local" ? l : s;
c.fixed = true;
// Before we duplicate the command, we must ensure
// that x (as above) still exists on the side which is to
// receive the duplicate command. If it's been deleted,
// we can drop the R(x,y), as x's position is irrelevant.
// See #7379.
var nodeset = arg == "local" ? local : server;
if (nodeset.Node(c.nid, false, true) == null) {
return do_drop(arg == "local" ? "server" : "local");
}
Xmarks.LogWrite("Duplicating command from " + arg + ": " + c.toSource());
// This bit is tricky. Insert our copy of the command c in
// the appropriate place in set. The appropriate place is the first
// non-fixed spot after the current command.
for ( i++; i < set.length; ++i) {
if (!set[i].fixed)
break;
}
set.splice(i, 0, c);
l = -1;
breakOut = true;
return;
}
}
function NearestAncestor(nid, baseline, ns1, ns2) {
while (nid) {
var node = baseline.Node(nid, false, true);
if (!node) {
throw Error("NearestAncestor failed accessing " + nid);
}
if (ns1.Node(nid, false, true) && ns2.Node(nid, false, true)) {
return nid;
}
nid = node.pnid;
}
throw "Couldn't find a common ancestor!";
}
function ResurrectNodes(nid, baseline, source, target, sourceCS, sourceIndex,
del, delCS, delIndex, bnid) {
var nids = [nid];
var pnid = null;
while (nids.length) {
var nid = nids.shift();
var node = baseline.Node(nid);
var sourceNode = source.Node(nid, false, true);
var delpos = del.deleted.indexOf(nid);
// Process this nid only if:
// * If was deleted by the given delete commandi and
// * It still exists in the source and
// * Its has the same parent in baseline and source
// or it's the root of what we're resurrecting.
if (delpos >= 0 && sourceNode &&
(!pnid || sourceNode.pnid == node.pnid)) {
var args = sourceNode.GetSafeAttrs();
if (!pnid) {
// This is our root; override the pnid and bnid if given.
pnid = NearestAncestor(args.pnid, baseline, source, target);
args.pnid = pnid;
if (bnid) {
args.bnid = bnid;
}
}
sourceCS.set.splice(sourceIndex++, 0,
new Command("insert", nid, args));
// If this nid was to be modified, drop the command as
// it is now redundant (we're going to insert the full
// node in its final state with the new command added above).
forEach(sourceCS.set, function(cmd, index) {
if (cmd.action == 'update' && cmd.nid == nid) {
sourceCS.drop(index);
}
});
// Remove this nid from the list of nids deleted by the delete
// command. This prevents triggering conflicts between the
// delete command and the insert we just added above.
if (delpos >= 0) {
del.deleted.splice(delpos, 1);
}
if (node.children) {
forEach(node.children, function(nid) { nids.push(nid) } );
}
}
}
// Finally, if our delete command now no longer deletes anything,
// delete it!
if (!del.deleted.length) {
delCS.drop(delIndex);
}
return pnid;
}